class WeightedAdjacencyMatrixGraph:
    """
    WeightedAdjacencyMatrixGraph represents a graph using a weighted adjacency matrix.

    Attributes:
        num_nodes (int): The number of nodes in the graph.
        matrix (List[List[Union[float, str]]]): The adjacency matrix representing the graph.
        nodes (List[Node]): The list of nodes in the graph.
    """

    def __init__(self, num_nodes):
        """
        Initialize an empty weighted adjacency matrix graph with a given number of nodes.

        Args:
            num_nodes (int): The number of nodes in the graph.
        """
        self.num_nodes = num_nodes
        self.matrix = [[float('inf')] * num_nodes for _ in range(num_nodes)]
        self.nodes = []

    def add_node(self, data=None):
        """
        Add a node to the graph.

        Args:
            data: The data for the node. Defaults to None.

        Returns:
            Node: The newly added node.
        """
        node = Node(data)
        self.nodes.append(node)
        return node

    def add_edge(self, source, destination, weight):
        """
        Add a weighted edge between two nodes in the graph.

        Args:
            source (int): The index of the source node.
            destination (int): The index of the destination node.
            weight (float): The weight of the edge.
        """
        self.matrix[source][destination] = weight

    def remove_edge(self, source, destination):
        """
        Remove the edge between two nodes in the graph.

        Args:
            source (int): The index of the source node.
            destination (int): The index of the destination node.
        """
        self.matrix[source][destination] = float('inf')

    def get_weight(self, source, destination):
        """
        Get the weight of the edge between two nodes in the graph.

        Args:
            source (int): The index of the source node.
            destination (int): The index of the destination node.

        Returns:
            float: The weight of the edge between the nodes.
        """
        return self.matrix[source][destination]

    def remove_node(self, node):
        """
        Remove a node from the graph.

        Args:
            node (Node): The node to be removed.
        """
        if node.data is None:
            raise ValueError("Cannot remove a node without data.")
        if node not in self.nodes:
            raise ValueError("Node not found in the graph.")
        index = self.nodes.index(node)
        self.nodes.remove(node)
        self.matrix.pop(index)
        for row in self.matrix:
            row.pop(index)
        self.num_nodes -= 1

    def get_nodes(self):
        """
        Get a list of nodes in the graph.

        Returns:
            List[Node]: The list of nodes.
        """
        return self.nodes

    def __str__(self):
        """
        Return a string representation of the graph.

        Returns:
            str: A string representation of the graph.
        """
        max_width = max(len(str(weight)) for row in self.matrix for weight in row)
        lines = []
        for row in self.matrix:
            line = [str(weight).ljust(max_width) for weight in row]
            lines.append(" ".join(line))
        return "\n".join(lines)

    def __repr__(self):
        """
        Return a string representation of the WeightedAdjacencyMatrixGraph object.

        Returns:
            str: A string representation of the object.
        """
        return f"Graph(num_nodes={self.num_nodes})"


class Node:
    """
    Node represents a node in a graph.

    Attributes:
        data: The data stored in the node.
    """

    def __init__(self, data=None):
        """
        Initialize a node with data.

        Args:
            data: The data for the node. Defaults to None.
        """
        self.data = data

    def __str__(self):
        """
        Return a string representation of the node.

        Returns:
            str: A string representation of the node.
        """
        return str(self.data)


if __name__ == "__main__":
    # Create a new weighted adjacency matrix graph
    graph = WeightedAdjacencyMatrixGraph(5)

    # Add nodes to the graph
    nodeA = graph.add_node("A")
    nodeB = graph.add_node("B")
    nodeC = graph.add_node("C")
    nodeD = graph.add_node("D")
    nodeE = graph.add_node("E")

    # Add edges between nodes with weights
    graph.add_edge(0, 1, 2.5)
    graph.add_edge(0, 2, 1.8)
    graph.add_edge(1, 3, 3.7)
    graph.add_edge(2, 3, 4.2)
    graph.add_edge(3, 4, 5.1)

    # Print the graph structure
    print("Graph structure:")
    print(graph)
    # Output:
    #  inf    2.5    1.8    inf    inf
    #  2.5    inf    inf    3.7    inf
    #  1.8    inf    inf    4.2    inf
    #  inf    3.7    4.2    inf    5.1
    #  inf    inf    inf    5.1    inf

    # Output the graph object representation
    print(repr(graph))
    # Output:
    # WeightedAdjacencyMatrixGraph(num_nodes=5, matrix=[[inf, 2.5, 1.8, inf, inf], [2.5, inf, inf, 3.7, inf], [1.8, inf, inf, 4.2, inf], [inf, 3.7, 4.2, inf, 5.1], [inf, inf, inf, 5.1, inf]], nodes=[A, B, C, D, E])

    # Remove an edge from the graph
    graph.remove_edge(1, 3)

    # Print the graph structure after removing an edge
    print("Graph structure after removing an edge:")
    print(graph)
    # Output:
    #  inf    2.5    1.8    inf    inf
    #  2.5    inf    inf    inf    inf
    #  1.8    inf    inf    4.2    inf
    #  inf    inf    4.2    inf    5.1
    #  inf    inf    inf    5.1    inf

    # Remove a node from the graph
    graph.remove_node(nodeC)

    # Print the graph structure after removing a node
    print("Graph structure after removing a node:")
    print(graph)
    # Output:
    #  inf    2.5    inf    inf
    #  2.5    inf    inf    inf
    #  inf    inf    inf    5.1
    #  inf    inf    5.1    inf

    # Output the graph object representation
    print(repr(graph))
    # Output:
    # WeightedAdjacencyMatrixGraph(num_nodes=4, matrix=[[inf, 2.5, inf, inf], [2.5, inf, inf, inf], [inf, inf, inf, 5.1], [inf, inf, 5.1, inf]], nodes=[A, B, D, E])
